/**
 * 
 */
package org.vsg.common.orm.mybatis;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;

import javax.sql.DataSource;

import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * @author Bill vison
 * 
 */
public class SqlSessionFactoryBean extends
		org.mybatis.spring.SqlSessionFactoryBean {

	private Resource configLocation;

	private Resource[] mapperLocations;

	private Resource[] mapperDomains;

	private DataSource dataSource;

	private Class<? extends TransactionFactory> transactionFactoryClass = SpringManagedTransactionFactory.class;

	private Properties transactionFactoryProperties;

	private Properties configurationProperties;

	private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

	private SqlSessionFactory sqlSessionFactory;

	private String environment = SqlSessionFactoryBean.class.getSimpleName();

	public SqlSessionFactoryBean() {
	}

	/**
	 * set the logger for develope code , do not use System.print(ln) to out
	 * print message.
	 */
	protected static final Logger logger = LoggerFactory
			.getLogger(SqlSessionFactoryBean.class);

	public Resource[] getMapperDomains() {
		return mapperDomains;
	}

	public void setMapperDomains(Resource[] mapperDomains) {
		this.mapperDomains = mapperDomains;
	}

	/**
	 * Set the location of the MyBatis SqlSessionFactory config file. A typical
	 * value is "WEB-INF/mybatis-configuration.xml".
	 */
	public void setConfigLocation(Resource configLocation) {
		this.configLocation = configLocation;
	}

	/**
	 * Set locations of MyBatis mapper files that are going to be merged into
	 * the SqlSessionFactory configuration at runtime.
	 * 
	 * This is an alternative to specifying "&lt;sqlmapper&gt;" entries in an
	 * MyBatis config file. This property being based on Spring's resource
	 * abstraction also allows for specifying resource patterns here: e.g.
	 * "classpath*:sqlmap/*-mapper.xml".
	 */
	public void setMapperLocations(Resource[] mapperLocations) {
		this.mapperLocations = mapperLocations;
	}

	/**
	 * Set optional properties to be passed into the SqlSession configuration,
	 * as alternative to a <code>&lt;properties&gt;</code> tag in the
	 * configuration xml file. This will be used to resolve placeholders in the
	 * config file.
	 * 
	 * @see org.apache.ibatis.session.Configuration#getVariables
	 */
	public void setConfigurationProperties(
			Properties sqlSessionFactoryProperties) {
		this.configurationProperties = sqlSessionFactoryProperties;
	}

	/**
	 * Set the JDBC DataSource that this instance should manage transactions
	 * for. The DataSource should match the one used by the SqlSessionFactory:
	 * for example, you could specify the same JNDI DataSource for both.
	 * 
	 * A transactional JDBC Connection for this DataSource will be provided to
	 * application code accessing this DataSource directly via DataSourceUtils
	 * or DataSourceTransactionManager.
	 * 
	 * The DataSource specified here should be the target DataSource to manage
	 * transactions for, not a TransactionAwareDataSourceProxy. Only data access
	 * code may work with TransactionAwareDataSourceProxy, while the transaction
	 * manager needs to work on the underlying target DataSource. If there's
	 * nevertheless a TransactionAwareDataSourceProxy passed in, it will be
	 * unwrapped to extract its target DataSource.
	 * 
	 * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
	 * @see org.springframework.jdbc.datasource.DataSourceUtils
	 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
	 */
	public void setDataSource(DataSource dataSource) {
		if (dataSource instanceof TransactionAwareDataSourceProxy) {
			// If we got a TransactionAwareDataSourceProxy, we need to perform
			// transactions for its underlying target DataSource, else data
			// access code won't see properly exposed transactions (i.e.
			// transactions for the target DataSource).
			this.dataSource = ((TransactionAwareDataSourceProxy) dataSource)
					.getTargetDataSource();
		} else {
			this.dataSource = dataSource;
		}
	}

	/**
	 * Sets the SqlSessionFactoryBuilder to use when creating the
	 * SqlSessionFactory.
	 * 
	 * This is mainly meant for testing so that mock SqlSessionFactory classes
	 * can be injected. By default, SqlSessionFactoryBuilder creates
	 * <code>DefaultSqlSessionFactory<code> instances.
	 * 
	 * @see org.apache.ibatis.session.SqlSessionFactoryBuilder
	 */
	public void setSqlSessionFactoryBuilder(
			SqlSessionFactoryBuilder sqlSessionFactoryBuilder) {
		this.sqlSessionFactoryBuilder = sqlSessionFactoryBuilder;
	}

	/**
	 * Set the MyBatis TransactionFactory class to use. Default is
	 * <code>SpringManagedTransactionFactory</code>.
	 * 
	 * The default SpringManagedTransactionFactory should be appropriate for all
	 * cases: be it Spring transaction management, EJB CMT or plain JTA. If
	 * there is no active transaction, SqlSession operations will execute SQL
	 * statements non-transactionally.
	 * 
	 * <b>It is strongly recommended to use the default TransactionFactory.</b>
	 * If not used, any attempt at getting an SqlSession through Spring's
	 * MyBatis framework will throw an exception if a transaction is active.
	 * 
	 * @see #setDataSource
	 * @see #setTransactionFactoryProperties(java.util.Properties)
	 * @see org.apache.ibatis.transaction.TransactionFactory
	 * @see org.mybatis.spring.transaction.SpringManagedTransactionFactory
	 * @see org.apache.ibatis.transaction.Transaction
	 * @see org.mybatis.spring.SqlSessionUtils#getSqlSession(SqlSessionFactory,
	 *      DataSource, org.apache.ibatis.session.ExecutorType)
	 * @param <TF>
	 *            the MyBatis TransactionFactory type
	 * @param transactionFactoryClass
	 *            the MyBatis TransactionFactory class to use
	 */
	public <TF extends TransactionFactory> void setTransactionFactoryClass(
			Class<TF> transactionFactoryClass) {
		this.transactionFactoryClass = transactionFactoryClass;
	}

	/**
	 * Set properties to be passed to the TransactionFactory instance used by
	 * this SqlSessionFactory.
	 * 
	 * The default SpringManagedTransactionFactory does not have any user
	 * configurable properties.
	 * 
	 * @see org.apache.ibatis.transaction.TransactionFactory#setProperties(java.util.Properties)
	 * @see org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory
	 * @see org.apache.ibatis.transaction.managed.ManagedTransactionFactory
	 */
	public void setTransactionFactoryProperties(
			Properties transactionFactoryProperties) {
		this.transactionFactoryProperties = transactionFactoryProperties;
	}

	/**
	 * <b>NOTE:</b> This class <em>overrides</em> any Environment you have set
	 * in the MyBatis config file. This is used only as a placeholder name. The
	 * default value is <code>SqlSessionFactoryBean.class.getSimpleName()</code>
	 * .
	 * 
	 * @param environment
	 *            the environment name
	 */
	public void setEnvironment(String environment) {
		this.environment = environment;
	}

	public void afterPropertiesSet() throws Exception {
		Assert.notNull(dataSource, "Property 'dataSource' is required");
		Assert.notNull(sqlSessionFactoryBuilder,
				"Property 'sqlSessionFactoryBuilder' is required");
		Assert.notNull(transactionFactoryClass,
				"Property 'transactionFactoryClass' is required");

		sqlSessionFactory = buildSqlSessionFactory();
	}

	/**
	 * Build a SqlSessionFactory instance.
	 * 
	 * The default implementation uses the standard MyBatis
	 * {@link XMLConfigBuilder} API to build a SqlSessionFactory instance based
	 * on an Reader.
	 * 
	 * @see org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
	 * 
	 * @return SqlSessionFactory
	 * 
	 * @throws IOException
	 *             if loading the config file failed
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 */
	protected SqlSessionFactory buildSqlSessionFactory() throws IOException,
			IllegalAccessException, InstantiationException {

		XMLConfigBuilder xmlConfigBuilder;
		Configuration configuration;

		if (configLocation != null) {
			Reader reader = null;
			try {
				reader = new InputStreamReader(configLocation.getInputStream());
				// Null environment causes the configuration to use the default.
				// This will be overwritten below regardless.
				xmlConfigBuilder = new XMLConfigBuilder(reader, null,
						configurationProperties);
				configuration = xmlConfigBuilder.parse();
			} catch (IOException ex) {
				throw new NestedIOException("Failed to parse config resource: "
						+ configLocation, ex);
			} finally {
				if (reader != null) {
					try {
						reader.close();
					} catch (IOException ignored) {
					}
				}
			}

			if (logger.isDebugEnabled()) {
				logger.debug("Parsed configuration file: '" + configLocation
						+ "'");
			}
		} else {
			if (logger.isDebugEnabled()) {
				logger.debug("Property 'configLocation' not specified, using default MyBatis Configuration");
			}
			configuration = new Configuration();
		}

		TransactionFactory transactionFactory = this.transactionFactoryClass
				.newInstance(); // expose IllegalAccessException,
								// InstantiationException

		transactionFactory.setProperties(transactionFactoryProperties);
		Environment environment = new Environment(this.environment,
				transactionFactory, this.dataSource);

		configuration.setEnvironment(environment);

		ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(
				this.getClass().getClassLoader());

		List<Resource> loaedResources = new Vector<Resource>();

		// --- edit handle mapper location ----
		if (!ObjectUtils.isEmpty(mapperDomains)) {

			Map<String, XNode> sqlFragments = new HashMap<String, XNode>();

			SAXReader saxReader = new SAXReader();

			List<String> checkLoadRecords = new Vector<String>();

			for (Resource mapperDomainsLocation : mapperDomains) {
				if (mapperDomainsLocation == null) {
					continue;
				}

				// MyBatis holds a Map using "resource" name as a key.
				// If a mapper file is loaded, it searches for a mapper
				// interface type.
				// If the type is found then it tries to load the mapper file
				// again looking for this:
				//
				// String xmlResource = type.getName().replace('.', '/') +
				// ".xml";
				//
				// So if a mapper interface exists, resource cannot be an
				// absolute path.
				// Otherwise MyBatis will throw an exception because
				// it will load both a mapper interface and the mapper xml file,
				// and throw an exception telling that a mapperStatement cannot
				// be loaded twice.

				String path;
				if (mapperDomainsLocation instanceof ClassPathResource) {
					path = ((ClassPathResource) mapperDomainsLocation)
							.getPath();
				} else {
					// this won't work if there is also a mapper interface in
					// classpath
					path = mapperDomainsLocation.toString();
				}
				try {
					Document doc = saxReader.read(mapperDomainsLocation
							.getURL());

					Element rootElement = doc.getRootElement();

					List<Element> eles = rootElement.elements("value");

					Element elem = null;
					for (Iterator<Element> elemsIter = eles.iterator(); elemsIter
							.hasNext();) {
						elem = elemsIter.next();
						String subPath = elem.getText();
						if (checkLoadRecords.contains(elem.getText())) {
							continue;
						}

						checkLoadRecords.add(subPath);

						Resource[] res = resourcePatternResolver
								.getResources(subPath);

						// --- red the resource ---
						for (Resource mapperLocation : res) {

							if (mapperLocation instanceof ClassPathResource) {
								path = ((ClassPathResource) mapperLocation)
										.getPath();
							} else {
								// this won't work if there is also a mapper
								// interface in classpath
								path = mapperLocation.toString();
							}

							Reader reader = null;
							try {
								if (loaedResources.contains(mapperLocation)) {
									continue;
								}
								reader = new InputStreamReader(
										mapperLocation.getInputStream());
								XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
										reader, configuration, path,
										sqlFragments);
								xmlMapperBuilder.parse();
								logger.debug("Load Resource " + mapperLocation
										+ " ok! ");
								loaedResources.add(mapperLocation);

							} catch (Exception e) {
								throw new NestedIOException(
										"Failed to parse mapping resource: '"
												+ mapperLocation + "'", e);
							} finally {
								if (reader != null) {
									try {
										reader.close();
									} catch (IOException ignored) {
									}
								}
							}

							if (logger.isDebugEnabled()) {
								logger.debug("Parsed mapper file: '"
										+ mapperLocation + "'");
							}
						}

					}

				} catch (DocumentException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}
		} else {
			if (logger.isDebugEnabled()) {
				logger.debug("Property 'mapperLocations' was not specified, only MyBatis mapper files specified in the config xml were loaded");
			}
		}
		
		
		this.mapperLocations = loaedResources.toArray(new Resource[0]);
		

		return sqlSessionFactoryBuilder.build(configuration);
	}

	// --- use dom4j ---
	private void parseDomains(Document doc, Collection<String> mapperFileList) {
		Element rootElement = doc.getRootElement();

		List<Element> eles = rootElement.elements("value");

		Element elem = null;
		for (Iterator<Element> elemsIter = eles.iterator(); elemsIter.hasNext();) {
			elem = elemsIter.next();

			if (!mapperFileList.contains(elem.getText())) {
				mapperFileList.add(elem.getText());
			}

		}
	}

	/**
	 * {@inheritDoc}
	 */
	public SqlSessionFactory getObject() throws Exception {
		if (sqlSessionFactory == null) {
			afterPropertiesSet();
		}

		return sqlSessionFactory;
	}

	/**
	 * {@inheritDoc}
	 */
	public Class<? extends SqlSessionFactory> getObjectType() {
		return sqlSessionFactory == null ? SqlSessionFactory.class
				: sqlSessionFactory.getClass();
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isSingleton() {
		return true;
	}

}
